/*
 *  Copyright 2011-2012 Redia A/S
 * 
 *  This file is part of Redias Android Utilities.
 *
 *  Redias Android Utilities is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Redias Android Utilities is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with Redias Android Utilities.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.redia.lib.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import dk.redia.lib.log.Log;
import dk.redia.lib.model.cache.InfoGalleriCache;
import dk.redia.lib.model.cache.InfoGalleriCacheListener;
import dk.redia.lib.utils.AppApplication;
import dk.redia.lib.utils.AppConfiguration;
import dk.redia.lib.xmlrpc.InfoGalleriXmlRpcClient;
import dk.redia.lib.xmlrpc.XmlRpcClientResultListener;
import dk.redia.lib.xmlrpc.XmlRpcResultDumper;
import dk.redia.lib.xmlrpc.XmlRpcResultUtils;

public class InfoGalleriModel extends Observable implements InfoGalleriCacheListener {
	
	private static InfoGalleriModel instance;

	private static final String logTag = "InfoGalleriModel";

	private List<Observer> infoObjectListObservers;
	private Map<AbsoluteId, Channel> channels;
	private int remainingChannelInfoObjectRequests = 0;
	private Map<AbsoluteId, Channel> newChannels; // Where channels are stored while they are being populated
	private boolean up;
	private int modelRefreshInterval;
	
	public static InfoGalleriModel getInstance() {
		if (instance==null) {
			instance = new InfoGalleriModel();
		}
		return instance;
	}
	
	public InfoGalleriModel() {
		this.infoObjectListObservers = new ArrayList<Observer>();
		this.channels = new HashMap<AbsoluteId, Channel>();
		this.newChannels = new HashMap<AbsoluteId, Channel>();
		this.up = false;
		this.modelRefreshInterval = AppConfiguration.getInstance().getInfoGalleriModelRefreshInterval();
		InfoGalleriCache.getInstance().addListener(this);

		
		// Wait this long initially, before synchronizing infogalleri content from online
		// web service. This should allow the cache to populate the view initially.
		new WaitTask(modelRefreshInterval).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

	}
	
	public void cacheUp() {
		Log.d(InfoGalleriModel.logTag, "Received word that the cache is up. Loading cached data and notifying listeners");
		List<Channel> cs = InfoGalleriCache.getInstance().getChannels(); // FIXME Should use resource data
		
		for (Channel channel : cs) {
			updateChannel(channel, true);
		}
		this.up = true;
		//notifyInfoObjectListObservers(); // commented
		refresh();
	}
	
	public void addInfoObjectsObserver(Observer o) {
		this.infoObjectListObservers.add(o);
	}
	
	public void removeInfoObjectsObserver(Observer o) {
		this.infoObjectListObservers.remove(o);
	}
	
	public void refresh() {
		if(isNetworkOnline()) {
			InfoGalleriXmlRpcClient.getInstance().getGalleryChannels(
					AppConfiguration.getInstance().getCustomerId(), 
					AppConfiguration.getInstance().getGalleryId(), 
					new GetGalleryChannelsResultListener());
			new WaitTask(modelRefreshInterval).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
		}
	}
	
	private void updateChannel(Channel c, boolean writeToCache) {
		channels.put(c.getAbsouteId(), c);
		//this.notifyInfoObjectListObservers(); // commented
		if (writeToCache) {
			InfoGalleriCache.getInstance().saveChannel(c);
		}
	}
	
	private void notifyInfoObjectListObservers() {
		// Copy observer list to avoid concurrent modification exceptions
		// if observers decide to unregister during call to update below
		List<Observer> cp = new ArrayList<Observer>(infoObjectListObservers);
		for (Observer o : cp) {
			o.update(this, null);
		}
	}
	
	private class WaitTask extends AsyncTask<Object, Object, Object> {

		private long delay;
		
		private static final String logTag = "WaitTask";
		
		public WaitTask(long delay) {
			super();
			this.delay = delay;
		}
		
		public WaitTask() {
			super();
			this.delay = 1000*60*30;
		}
		
		@Override
		protected Object doInBackground(Object... params) {
			Thread.currentThread().setName("WaitTask");
			try {
				Log.d(WaitTask.logTag, "Sleeping for " +  this.delay + " milliseconds. Good night.");
				Thread.sleep(this.delay);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return null;
		}
		
		@Override
		protected void onPostExecute(Object result) {
			Log.d(WaitTask.logTag, "Woke up after " + this.delay + " milliseconds sleep. 'morning.");
			InfoGalleriModel.getInstance().refresh();
		}
		
	}
	
	private class GetGalleryChannelsResultListener implements XmlRpcClientResultListener {

		public void callCompleted(Object result) {
			Log.d(logTag, "Dumping result of getGalleryChannels:");
			XmlRpcResultDumper.dump(result);
			Object[] channelObjs = XmlRpcResultUtils.unpackArray(result);
			if (channelObjs==null) {
				Log.e(logTag, "channelObjs is null in GetGalleryChannelsResultListener.callCompleted");
				return;
			}
			newChannels.clear();
			remainingChannelInfoObjectRequests = channelObjs.length;
			for (Object channelObj : channelObjs) {
				Map<String, Object> galleryMap = XmlRpcResultUtils.unpackStruct(channelObj);
				Channel c = Channel.fromMap(galleryMap);
				// Channel will get added into the channels map once all info objects related to the 
				// channel have been fetched below
				if (c!=null) {
					InfoGalleriXmlRpcClient.getInstance().getChannelInfoObjects(
							AppConfiguration.getInstance().getCustomerId(),
							c.getId(), 
							new GetChannelInfoObjectsResultListener(c)
					);
				} else {
					Log.w(logTag, "Failed to create channel object from map. Skipping object.");
				}
			}
		}

		public void callFailed(String message) {
			Log.w(logTag, "Failed to call getGalleryChannels: "+ message);
		}
	}

	private class GetChannelInfoObjectsResultListener implements XmlRpcClientResultListener {

		private Channel c;
		
		public GetChannelInfoObjectsResultListener(Channel c) {
			this.c = c;
		}
		
		public void callCompleted(Object result) {
			XmlRpcResultDumper.dump(result);
			Log.d(logTag, "Received result of call to getChannelInfoObjects for channel with id " + c.getId());
			remainingChannelInfoObjectRequests--;
//			System.err.println("Dumping result of getChannelInfoObjects:");
//			XmlRpcResultDumper.dump(result);
			Map<String, Object> map = XmlRpcResultUtils.unpackStruct(result);
			if (map==null) {
				Log.d(logTag, "map is null in GetChannelInfoObjectsResultListener.callCompleted");
				return;
			}
			if (!map.containsKey("ordering")) {
				Log.e(logTag, "No ordering in result from getChannelInfoObjects");
				return;
			}
			if (!map.containsKey("data")) {
				Log.w(logTag, "No data in result from getChannelInfoObjects");
				return;
			}
			Object[] ordering = XmlRpcResultUtils.unpackArray(map.get("ordering"));
			String[] order = new String[ordering.length];
			for(int i = 0; i < order.length; i++) {
				order[i] = ordering[i].toString();
			}
			InfoGalleriCache.getInstance().saveOrdering(c.getId(), order);
			Map<String, Object> data = XmlRpcResultUtils.unpackStruct(map.get("data"));
			this.c.removeAllInfoObjects();
			for (Object infoObjectIdObj : ordering) {
				String infoObjectId = (String)infoObjectIdObj;
				Object infoObjectXmlObj = data.get(infoObjectId);
				String infoObjectXml = (String)infoObjectXmlObj;
//				System.out.println("Ready to parse " + infoObjectXml.length() + " chars of XML for object with id " + infoObjectId);
				InfoObject io = InfoObject.fromXml(infoObjectXml);				
				if (io!=null) {
					try {
						io.getAbsoluteId();
					} catch (RuntimeException e) {
						Log.e(logTag, "Unable to get id from parsed info object. Skipping: " + e.getMessage());
					}
					Log.d(logTag, "Successfully created info object instance: " + io.getHeadline("dk"));
					this.c.addInfoObject(io);
				} else {
					Log.e(logTag, "Failed to create info object from XML: " + infoObjectXml);
				}
			}
			newChannels.put(this.c.getAbsouteId(), this.c);
			InfoGalleriModel.getInstance().updateChannel(this.c, true); // uncommented
			checkIfLast();
		}

		public void callFailed(String message) {
			Log.w(logTag, "Failed to call getChannelInfoObjects: " + message);
			remainingChannelInfoObjectRequests--;
			checkIfLast();
		}
		
		public void checkIfLast() {
			if (remainingChannelInfoObjectRequests<=0) {
				Log.d(InfoGalleriModel.logTag, "No remaining channel download requests. Overwriting channels with newChannels");
				channels = newChannels;
				notifyInfoObjectListObservers();
			} else {
				Log.d(InfoGalleriModel.logTag, "There are " + remainingChannelInfoObjectRequests + " still remaining. Not overwriting channels");
			}
		}
	}

	public List<InfoObject> getInfoObjects() {
		return getInfoObjects(InfoObject.TYPE_ALL);
	}
	
	/**
	 * Get a list of info objects of the specified type(s)
	 * 
	 * @param type Bitmask identifying requested types. OR InfoObject.TYPE* constants defines the types.
	 * @return The requested list of info objects
	 */
	public List<InfoObject> getInfoObjects(int type) {
		List<InfoObject> ios = new ArrayList<InfoObject>();
		if(isNetworkOnline()) {
			// FIXME This is an absolutely horrible implementation.
			
			for (Channel c : this.channels.values()) {
				for (InfoObject io : c.getInfoObjects().values()) {
					// Check if specified bitmask contains the current objects
					// type
					if ((type & io.getType())>0) {
						ios.add(io);
					}
				}
			}
		} else {
			ios = getCachedInfoObjects(type);
		}
		Log.d(InfoGalleriModel.logTag , "InfoGalleriModel.getInfoObjects found " + ios.size() + " info objects in " + this.channels.size() + " channels");
		return ios;
	}
	
	public List<InfoObject> getCachedInfoObjects(int type) {
		return InfoGalleriCache.getInstance().loadInfoObjects();
	}
	
	public InfoObject getCachedInfoObject(AbsoluteId id) {
		return InfoGalleriCache.getInstance().loadInfoObject(id);
	}
/*
	public InfoObject loadInfoObject(AbsoluteId ioid) {
		Log.d(InfoGalleriModel.logTag, "Loading single info object with id " + ioid);
		return InfoGalleriCache.getInstance().loadInfoObject(ioid);
	}
*/
	
	public List<Channel> getChannels() {
		return InfoGalleriCache.getInstance().getChannels();
	}
	
	public boolean isUp() {
		return up;
	}
	
	/*public List<InfoObject> getChannelInfoObject(AbsoluteId channelId) {
		return InfoGalleriCache.getInstance().loadChannelInfoObjects(channelId);
	}*/

	public InfoObject getInfoObject(AbsoluteId absId) {
		if(isNetworkOnline()) {
		// FIXME This is an absolutely horrible implementation.
			for (Channel c : this.channels.values()) {
				for (InfoObject io : c.getInfoObjects().values()) {
					if (io.getAbsoluteId().equals(absId)) {
						return io;
					}
				}
			}			
		} else {
			return InfoGalleriCache.getInstance().loadInfoObject(absId);
		}
		Log.w(InfoGalleriModel.logTag, "Failed to find info object with id " + absId + " in InfoGalleriModel");
		return null;
	}
	
	   public boolean isNetworkOnline() {
		    boolean status = false;
		    try {
		        ConnectivityManager cm = (ConnectivityManager) AppApplication.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE);
		        NetworkInfo netInfo = cm.getNetworkInfo(0);
		        if (netInfo != null && netInfo.getState() == NetworkInfo.State.CONNECTED) {
		            status = true;
		        } else {
		            netInfo = cm.getNetworkInfo(1);
		            if (netInfo != null && netInfo.getState() == NetworkInfo.State.CONNECTED)
		                status= true;
		        }
		    } catch (Exception e) {
		        e.printStackTrace();  
		        return false;
		    }
	    	return status;
	   }
}
